Equivalent only when both sides are truly unit-normalized
The claim is true only under a precise condition: both the query vector and every stored vector must be normalized to unit length. Cosine similarity is defined as dot(a, b) / (||a|| * ||b||). If both ||a|| and ||b|| are exactly 1, the denominator is 1 and cosine reduces to dot(a, b). The rankings are then identical. The subtlety that candidates miss is that this equivalence is conditional, and in practice the condition is often violated in ways that are invisible because the search still returns plausible results. The first violation is unnormalized query vectors: if the client normalizes documents at ingest but not queries, the query's norm is not 1, and cosine and dot will rank differently because the dot product scales with the query norm. The second violation is unnormalized stored vectors: if a single document's vector was not normalized (a bug in an ingest path, a partial batch, a manual upsert), that document's score is off by its norm and its ranking shifts. The third, more subtle violation is the storage metric: if the collection is created with Distance.DOT but the vectors were normalized, and the query is normalized, the rankings match cosine. But if the collection is created with Distance.COSINE, Qdrant normalizes at query time (or assumes normalized inputs), and the behavior differs from a DOT collection with unnormalized vectors. The fourth subtlety is that even when the rankings are identical in exact arithmetic, floating-point rounding can produce tiny differences that change the order of near-tied results, which matters for recall measurement but not for user experience.
The mechanism that makes this matter is that the equivalence is about the ordering of scores, not about the scores themselves. Cosine and dot produce the same ordering when both sides are normalized, but they produce different scores - cosine is bounded to [-1, 1] while dot is unbounded. If you compare a score threshold (e.g. 'only return results with similarity > 0.8'), the threshold means different things under the two metrics and can change which results are returned. This is why a system that switches from DOT to COSINE, or vice versa, can appear to 'work' while actually returning different top-k in edge cases. The practical implication is that the metric must be chosen deliberately and applied consistently: normalize everything and use DOT, or use COSINE and let the engine handle normalization, but do not mix. The other practical implication is that a test asserting on exact scores will fail when the metric changes even if the ranking is the same, so tests should assert on rankings, not on raw scores.
Equivalence condition: both query and stored vectors normalized to unit length.
Unnormalized query: dot scales with the query norm; cosine does not.
Unnormalized stored vector: that document's ranking is wrong under dot.
Metric mismatch: a DOT collection with normalized vectors and a COSINE collection behave differently at the boundaries.
Score vs ranking: the rankings match, the scores do not; threshold filters behave differently.
Floating-point: near-ties can be ordered differently under the two metrics.
Recommendation: pick one metric, apply it consistently, and test on rankings not scores.
Verification: check ||v|| for a sample of stored vectors and the query vectors.
The trade-off is between the simplicity of 'normalize everything and use dot' and the robustness of 'use cosine and let the engine normalize'. The first is faster (no normalization at query time) but requires discipline in the ingest path. The second is safer (the engine handles normalization) but adds a small per-query cost. In practice, most modern embedding models output normalized vectors, and using DOT is a common choice, but the ingest path must be verified. The common mistakes are: (1) assuming the equivalence is unconditional; (2) normalizing at ingest but not at query (or vice versa); (3) switching the metric without re-normalizing and not noticing because the results still 'look right'; (4) comparing scores across metrics as if they were comparable; (5) not testing the edge case of an unnormalized vector in an otherwise normalized collection. Version note: the behavior of Distance.COSINE and Distance.DOT, and whether the engine normalizes internally, has been stable across recent Qdrant releases but the details of the implementation may differ. Verify by testing with a known unnormalized vector.
Version-dependent: the exact behavior of Distance.COSINE (whether the engine normalizes both sides, one side, or assumes normalized input) is version-specific. In practice, Qdrant's cosine distance normalizes both sides, so the equivalence holds for the ranking, but the scores are cosine scores, not dot scores. Verify the behavior on your version by comparing the scores from a COSINE collection with the hand-computed cosine similarity.
You switch from cosine to dot product and the rankings change slightly. Explain why.
A teammate says the two are always equivalent. Explain the condition under which they are.
Your recall measurement differs between a COSINE and a DOT collection with the same vectors. Diagnose the cause and describe the fix.
You need to verify that all stored vectors are normalized. Describe the check and how you would run it on a large collection.
Design a test suite that catches unnormalized vectors and metric mismatches before they reach production.
You are migrating from a COSINE collection to a DOT collection for performance. Describe the validation that ensures the ranking is unchanged.
Derive the exact condition under which the top-k under cosine and dot are guaranteed to be identical, including the case of ties.
You are designing a system that must support both cosine and dot collections transparently. Describe the abstraction and the validation.